package top.jle.jpa.basic.dao;

import jakarta.annotation.Resource;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import org.apache.commons.lang3.ArrayUtils;
import org.hibernate.Hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.query.NativeQuery;
import org.hibernate.query.TupleTransformer;
import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import top.jle.jpa.basic.entities.BaseEntity;
import top.jle.jpa.basic.entities.Entity;
import top.jle.jpa.basic.entities.TimeMarkedEntity;

import java.io.Serializable;
import java.util.*;


@Repository("entityDao")
public class EntityDaoImpl implements EntityDao {

    /**
     * 日志
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(EntityDaoImpl.class);
    @PersistenceContext
    private EntityManager entityManager;
    @Resource
    private EntityManagerFactory entityManagerFactory;


    @Autowired
    private DetachedCriteriaFactory detachedCriteriaFactory;

    public EntityManager getTemplate() {
        return entityManager;
    }

    @Override
    @Transactional
    public <T extends Serializable> T merge(T entity) {
        this.markCreateTimeIfPossible(entity);
        this.markLastModifyTimeIfPossible(entity);
        return this.getTemplate().merge(entity);
    }

    @Override
    public <T extends Serializable> void evict(T entity) {
        this.getTemplate().detach(entity);
    }

    @Override
    @Transactional
    public <T extends Serializable> void updateAndRefresh(T entity) {
        this.markLastModifyTimeIfPossible(entity);
        this.getTemplate().merge(entity);
        this.getTemplate().flush();
        this.getTemplate().refresh(entity);
    }

    @Override
    @Transactional
    public <T extends Serializable> void refresh(T entity) {
        this.getTemplate().refresh(entity);
    }

    @Override
    @Transactional
    public <T extends Serializable> void save(T entity) {
        this.markCreateTimeIfPossible(entity);
        this.markLastModifyTimeIfPossible(entity);
        this.getTemplate().persist(entity);
    }

    @Override
    public void clear() {
        this.getTemplate().clear();
    }

    @Override
    public void flush() {
        this.getTemplate().flush();
    }

    @Override
    public <T extends Serializable> void initProxyObject(T proxyEntity) {
        Hibernate.initialize(proxyEntity);
    }

    @Override
    @Transactional
    public <T extends Serializable> void update(T entity) {
        this.markLastModifyTimeIfPossible(entity);
        this.getTemplate().merge(entity);
    }


    @Override
    public void updateByHQL(final String hql, final Object... value) {
        Query query = this.buildQuery(hql,value);
        query.executeUpdate();
    }
    private Query buildQuery(final String hql, final Object... value){
        Query query = this.getTemplate().createQuery(hql);
        if (ArrayUtils.isNotEmpty(value)) {
            for (int i = 0; i < value.length; i++) {
                if(value[i]==null){
                    continue;
                }
                query.setParameter(i+1, value[i]);
            }
        }
        return query;
    }
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public <T extends Serializable> void saveOrUpdate(T entity) {
        this.markCreateTimeIfPossible(entity);
        this.markLastModifyTimeIfPossible(entity);
        if(entity instanceof Entity e){
            if(e.getId()==null){
                this.getTemplate().persist(e);
            }else{
                this.getTemplate().merge(e);
            }
        }
    }

    @Override
    @Transactional
    public <T extends Serializable> void delete(T entity) {
        this.markLastModifyTimeIfPossible(entity);
        this.getTemplate().remove(entity);
    }

    @Override
    @Transactional
    public <T extends Serializable> void deleteById(Class<T> clazz, long id) {
        T entity = this.findById(clazz, id);
        if (entity != null) {
            this.delete(entity);
        }
    }

    @Override
    @Transactional
    public <T extends Serializable> void deleteByIds(Class<T> clazz, Long[] ids) {
        for (Long id : ids) {
            this.deleteById(clazz, id);
        }
        this.flush();
    }

    @Override
    @Transactional
    public void deleteByIds(String className, Long[] ids) {
        for (Long id : ids) {
            Serializable entity = this.findById(className, id);
            if (entity != null) {
                this.delete(entity);
            }
        }
        this.flush();
    }

    @Override
    @Transactional
    public <T extends Serializable> int deleteAll(Class<T> clazz) {
        return this.getTemplate().createQuery(entityManager.getCriteriaBuilder().createCriteriaDelete(clazz)).executeUpdate();
    }


    @Override
    public void deleteBySQL(final String sql) {
        this.getTemplate().createNativeQuery(sql, Integer.class).executeUpdate();

    }

    @Override
    @Transactional
    public <T extends Serializable> void deleteByPrefilters(Class<T> clazz, PreFilter... preFilters) {
        HibernateCriteriaBuilder builder =	entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();
        CriteriaQuery<T> query = detachedCriteriaFactory.buildDetachedCriteria(clazz, builder, preFilters);
        CriteriaDelete<T> delete = entityManager.getCriteriaBuilder().createCriteriaDelete(clazz);
        delete.where(query.getRestriction());
        this.getTemplate().createQuery(delete).executeUpdate();
    }

    @Override
    @Transactional
    public <T extends Serializable> void deleteAll(Collection<T> collection) {
        collection.forEach(this.getTemplate()::remove);
        this.getTemplate().flush();
    }

    @Override
    public <T extends Serializable> Integer getCountByPrefilters(Class<T> clazz, PreFilter... preFilters) {
        CriteriaQuery<T> dc = getCriteriaQuery(clazz, preFilters);
        return this.getCountByDetachedCriteria(dc.distinct(false));
    }

    private <T extends Serializable> CriteriaQuery<T> getCriteriaQuery(Class<T> clazz, PreFilter[] preFilters) {
        HibernateCriteriaBuilder builder = entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();
        return detachedCriteriaFactory.buildDetachedCriteria(clazz, builder, preFilters);
    }

    @Override
    public <T extends Serializable> boolean existsByPreFilters(Class<T> clazz, PreFilter... preFilters) {
        CriteriaQuery<T> dc = getCriteriaQuery(clazz, preFilters);
        return this.existByDetachedCriteria(dc);
    }


    @Override
    public <T extends Serializable> T getReferenceById(Class<T> clazz, long id) {
        return getTemplate().getReference(clazz, id);
    }

    @Override
    public <T extends Serializable> Set<T> getReferencesByIds(Class<T> clazz, Long[] ids) {
        Set<T> set = new HashSet<>();
        if (ids != null) {
            for (Long id : ids) {
                set.add(this.getReferenceById(clazz, id));
            }
        }
        return set;
    }

    @Override
    public <T extends Serializable> T findById(Class<T> clazz, long id) {
        return this.getTemplate().find(clazz, id);
    }

    @Override
    public <T extends Serializable> List<T> findAll(Class<T> clazz, SortKey... sortKeys) {
        return this.findByPreFilter(clazz, sortKeys);
    }

    @Override
    public <T extends Serializable> List<T> findByPreFilter(Class<T> clazz, PreFilter... preFilters) {
        HibernateCriteriaBuilder builder =	entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();
        CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteria(clazz, builder, preFilters);
        return getTemplate().createQuery(dc).getResultList();
    }

    @Override
    public <T extends Serializable> List<T> findByPreFilter(Class<T> clazz, SortKey[] sortKeys,
                                       PreFilter... preFilters) {
        CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteriaWithOrder(clazz, sortKeys, preFilters);
        return this.getTemplate().createQuery(dc).getResultList();

    }


    @Override
    public <T extends Serializable> T findUniqueByPreFilter(Class<T> clazz,
                                       PreFilter... preFilters) {
        HibernateCriteriaBuilder builder =	entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();
        CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteria(clazz, builder, preFilters);
        return findUniqueByDetachedCriteria(dc);
    }

    @Override
    public <T extends Serializable> List<T> findByIds(Class<T> clazz, Long[] ids) {
        HibernateCriteriaBuilder builder =	entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();
        CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteria(clazz, builder, PreFilters.in(BaseEntity.PROP_ID, (Object[]) ids));
        return this.getTemplate().createQuery(dc).getResultList();
    }

    @Override
    @Transactional
    public <T extends Serializable> PagedResult<T> getPage(Class<T> clazz, PageRequest pageRequest,
                                      PreFilter... preFilters) {
        CriteriaQuery<T> listDc = detachedCriteriaFactory.buildDetachedCriteriaWithOrder(clazz, pageRequest, preFilters);

        return getResult(pageRequest, listDc);
    }

    private <T extends Serializable> PagedResult<T> getResult(PageRequest pageRequest, CriteriaQuery<T> listDc) {
        List<T> resultList = this.getTemplate().createQuery(listDc).setFirstResult(pageRequest.getOffset()).setMaxResults(pageRequest.getLimit()).getResultList();

        int totalCount = 0;
        // 如果pageRequest_a要求计算总行数,则计算之
        if (pageRequest.isCountTotal()) {
            totalCount = getCountByDetachedCriteria(listDc);
        }
        return buildPagedResult(pageRequest.getOffset(), resultList, totalCount, pageRequest.getLimit(), pageRequest.getCurrentPage());
    }


    @Override
    public <T extends Serializable> int getAllCount(Class<T> clazz) {
        final CriteriaQuery<T> dc = entityManager.getCriteriaBuilder().createQuery(clazz);
        return this.getCountByDetachedCriteria(dc);
    }

    @Override
    public <T extends Serializable> int getCountByPreFilter(Class<T> clazz, PreFilter... preFilters) {
        HibernateCriteriaBuilder builder =	entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();
        final CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteria(clazz, builder, preFilters);
        return this.getCountByDetachedCriteria(dc);
    }


    @SuppressWarnings("unchecked")
    @Override
    public <T extends Serializable> List<T> findByHQL(String hql, Object... values) {
        Query query = this.buildQuery(hql,values);
        return query.getResultList();

    }

    @SuppressWarnings({"unchecked"})
    @Override
    public <T extends Serializable> T findUniqueByHQL(final String hql, final Object... values) {
        Query query = buildQuery(hql,values);
        return (T) query.getResultList().stream().findFirst().orElse(null);
    }

    @Override
    public <T extends Serializable> PagedResult<T> getPageByClassName(String className,
                                             PageRequest pageRequest, PreFilter... preFilters) {
        CriteriaQuery<T> listDc =
                detachedCriteriaFactory.buildDetachedCriteriaByClassNameWithOrder(
                        className,
                        pageRequest.getOrderBy(),
                        preFilters);
        return getResult(pageRequest, listDc);

    }


    @Override
    @Transactional
    public <T extends Serializable> PagedResult<T> getPageByHQL(final String hql, final PageRequest pageRequest,
                                       final Object... values) {
        Query query = buildQuery(hql,values);
        @SuppressWarnings("unchecked")
        List<T> resultList = query.setFirstResult(pageRequest.getOffset()).setMaxResults(pageRequest.getLimit()).getResultList();
        int index = hql.toUpperCase().indexOf("FROM");
        String countHQL = "select count(1) "+hql.substring(index);
        Query countQuery = buildQuery(countHQL,values);
        long count = (long) countQuery.getSingleResult();
        return buildPagedResult(pageRequest.getOffset(), resultList, (int) count, pageRequest.getLimit(), pageRequest.getCurrentPage());
    }

    @Override
    public <T extends Serializable> PagedResult<T> getPageBySQL(final String sql, final PageRequest pageRequest,
                                       final Object... values) {
        return getPagedResult(sql, pageRequest,null, values);
    }

    private <T> NativeQuery<?> buildNativeQuery(String sql,Class<T> clazz,Object... values){
        TupleTransformer<HashMap<String,Object>> eachResultTransformer = null;
        Class<T> tClass = clazz;
        if(clazz!=null && Map.class.isAssignableFrom(clazz)){
            eachResultTransformer = (tuple, aliases) -> {
                HashMap<String,Object> result = new HashMap<>();
                for (int i = 0; i < aliases.length; i++) {
                    result.put(aliases[i],tuple[i]);
                }
                return result;
            };
            tClass = null;
        }
        @SuppressWarnings("unchecked")
        NativeQuery<T> query = (NativeQuery<T>) (tClass==null?this.getTemplate().createNativeQuery(sql):this.getTemplate().createNativeQuery(sql,clazz));
        if (ArrayUtils.isNotEmpty(values)) {
            for (int i = 0; i < values.length; i++) {
                query.setParameter(i+1, values[i]);
            }
        }
        return query.setTupleTransformer(eachResultTransformer);
    }

    @Override
    public <T extends Serializable> PagedResult<T> getPageBySQL(final String sql, final Class<T> clazz, final PageRequest pageRequest,
                                       final Object... values) {
        return getPagedResult(sql, pageRequest,clazz, values);
    }

    private <T extends Serializable> PagedResult<T> getPagedResult(String sql, PageRequest pageRequest,Class<T> clazz, Object... values) {

        Query query = this.buildNativeQuery(sql,clazz,values);
        @SuppressWarnings("unchecked")
        List<T> resultList = query.setFirstResult(pageRequest.getOffset()).setMaxResults(pageRequest.getLimit()).getResultList();
        int index = sql.toUpperCase().indexOf("FROM");
        String countSQL = "select count(1) "+sql.substring(index);
        Query countQuery = this.buildNativeQuery(countSQL,Integer.class,values);
        int count = (int) countQuery.getSingleResult();
        return buildPagedResult(pageRequest.getOffset(), resultList, count, pageRequest.getLimit(), pageRequest.getCurrentPage());
    }


    @Override
    public <T extends Serializable> T findById(String className, long id) {
        return this.findUniqueByHQL("select o from " + className + " o where o.id = ?1", id);
    }

    @Override
    public <T extends Serializable> T findUniqueByPreFilter(String className, SortKey[] sortKeys,
                                       PreFilter... preFilters) {
        final CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteriaByClassNameWithOrder(
                        className, sortKeys, preFilters);

        return findUniqueByDetachedCriteria(dc);
    }

    @Override
    public <T extends Serializable> List<T> findByPreFilter(String className, SortKey[] sortKeys,
                                       PreFilter... preFilters) {
        final CriteriaQuery<T> dc = detachedCriteriaFactory.buildDetachedCriteriaByClassNameWithOrder(className, sortKeys, preFilters);
        return this.getTemplate().createQuery(dc).getResultList();

    }


    @SuppressWarnings({"unchecked"})
    @Override
    public <T extends Serializable> List<T> findBySQL(final String sql, final Object... values) {
        Query query = this.buildNativeQuery(sql,null,values);
        return query.getResultList();
    }

    @SuppressWarnings({"unchecked"})
    @Override
    public <T extends Serializable> List<T> findBySQL( Class<T> clazz, String sql,   final Object... values) {
        Query query = this.buildNativeQuery(sql,clazz,values);
        return query.getResultList();
    }


    @Override
    public <T extends Serializable> void markCreateTimeIfPossible(T entity) {
        try {
            if (entity instanceof TimeMarkedEntity t && t.getCreateTime() == null) {
                t.setCreateTime(System.currentTimeMillis());
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

    }

    @Override
    public <T extends Serializable> void markLastModifyTimeIfPossible(T entity) {
        try {
            if (entity instanceof TimeMarkedEntity t) {

                t.setLastModifyTime(System.currentTimeMillis());
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }



    @Override
    public void deleteById(String className, Long id) {
        final String hql = "delete " + className + " o where o.id = ?1";
        this.updateByHQL(hql, id);
    }

    @Override
    public void execSQL(final String sql) {
        this.getTemplate().createNativeQuery(sql).executeUpdate();
    }


    private <T extends Serializable> T findUniqueByDetachedCriteria(final CriteriaQuery<T> detachedCriteria) {
        return this.getTemplate().createQuery(detachedCriteria).getResultList().stream().findFirst().orElse(null);
    }



    protected <T extends Serializable> PagedResult<T> buildPagedResult(int currOffset, List<T> resultList, int totalCount, int pageSize, int currentPage) {
        int nextOffset = currOffset;

        if (resultList != null) {
            nextOffset += resultList.size();
        }
        PagedResult<T> pagedResult = new PagedResult<>(resultList, nextOffset, totalCount, currentPage);
        pagedResult.setTotalPageNum((totalCount + pageSize - 1) / pageSize);
        pagedResult.setPageSize(pageSize);
        return pagedResult;
    }


    protected <T extends Serializable> int getCountByDetachedCriteria(final CriteriaQuery<T> detachedCriteria) {
        var sqmOriginalQuery = (SqmSelectStatement<T>) detachedCriteria;
        sqmOriginalQuery.producesUniqueResults();
        var countQuery = sqmOriginalQuery.createCountQuery();
        Long count =  this.getTemplate().createQuery(countQuery).getSingleResult();
        return count.intValue();
    }

    protected <T extends Serializable> boolean existByDetachedCriteria(final CriteriaQuery<T> detachedCriteria) {
        return !this.getTemplate().createQuery(detachedCriteria).setFirstResult(0).setMaxResults(1).getResultList().isEmpty();
    }
}
